package org.dcarew.logviewer;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Map;

import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.ui.IMemento;
import org.eclipse.ui.IViewSite;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.console.IOConsole;
import org.eclipse.ui.console.IOConsoleOutputStream;
import org.eclipse.ui.console.TextConsoleViewer;
import org.eclipse.ui.part.ViewPart;
import org.eclipse.ui.progress.IWorkbenchSiteProgressService;

// TODO: see also JavaStackTraceConsoleViewer and JavaStackTraceConsole

// TODO: show next to the console view - use a perspective extension

// TODO: need to do something like listen for text changes, and scroll to the end
// 

/**
 * 
 * 
 * @author Devon Carew
 */
public class LogViewerView
    extends ViewPart
    implements IEclipseLaunchListener
{
    public static final String      ID = "org.dcarew.logviewer.LogViewerView";
    
    private IOConsole               console;
    private TextConsoleViewer       textConsoleViewer;
    
    private IOConsoleOutputStream   out;
    
    private boolean                 dataWasWritten;
    
    private FileWatcher             fileWatcher;
    
    
    /**
     * 
     */
    public LogViewerView()
    {
        
    }





    @Override
    public void init(IViewSite site, IMemento memento)
        throws PartInitException
    {
        super.init(site, memento);
        
        createToolbar(site.getActionBars().getToolBarManager());
    }





    private void createToolbar(IToolBarManager toolBarManager)
    {
        toolBarManager.add(new ClearConsoleAction());
    }





    @Override
    public void createPartControl(Composite parent)
    {
        console = new IOConsole("Runtime Log", LogViewerPlugin.getImageDescriptor("jsbook_obj.gif"));
        console.setTabWidth(4);
        
        textConsoleViewer = new TextConsoleViewer(parent, console);
        textConsoleViewer.getTextWidget().setEditable(false);
        
        startListening();
    }





    @Override
    public void setFocus()
    {
        textConsoleViewer.getTextWidget().setFocus();
    }





    @Override
    public void dispose()
    {
        stopListening();
        stopWatchingFile();
        
        super.dispose();
    }





    @SuppressWarnings("unchecked")
    @Override
    public void launchStarted(ILaunch launch, ILaunchConfiguration launchConfiguration)
    {
        clear();
        
        out = console.newOutputStream();
        out.setActivateOnWrite(true);
        
        try
        {
            // launchConfiguration.getType().getName() == "Eclipse Application"
            // location=${workspace_loc}/../runtime-EclipseApplication
            
            Map launchAttributes = launchConfiguration.getAttributes();
            
            String location = (String)launchAttributes.get("location");
            
            CharSequence workspaceLocation = ResourcesPlugin.getWorkspace().getRoot().getLocation().toString();
            
            location = location.replace(
                    (CharSequence)"${workspace_loc}", workspaceLocation);
            
            watchFile(location);
        }
        catch (CoreException ce)
        {
            LogViewerPlugin.log(ce);
        }
    }




    private void watchFile(String filePath)
    {
        // /.metadata/.log
        
        stopWatchingFile();
        
        IPath path = new Path(filePath);
        path = path.append(".metadata");
        path = path.append(".log");
        
        final File file = path.toFile();
        
        fileWatcher = new FileWatcher(file);
        
        asyncExec(new Runnable() {
            public void run()
            {
                setContentDescription(file.toString());
            }
        });
    }


    private void stopWatchingFile()
    {
        if (fileWatcher != null)
            fileWatcher.dispose();
        fileWatcher = null;
    }


    @Override
    public void launchEnded(ILaunch launch, ILaunchConfiguration launchConfiguration)
    {
        if (dataWasWritten())
        {
            asyncExec(new Runnable() {
                @Override
                public void run()
                {
                    IWorkbenchSiteProgressService progressService = 
                        (IWorkbenchSiteProgressService)getSite().getAdapter(IWorkbenchSiteProgressService.class);
                    
                    progressService.decrementBusy();
                    progressService.warnOfContentChange();
                }
            });
        }
        
        stopWatchingFile();
    }


    private void asyncExec(Runnable runnable)
    {
        getSite().getShell().getDisplay().asyncExec(runnable);
    }
    
    
    private void clear()
    {
        console.clearConsole();
        
        dataWasWritten = false;
        
        stopWatchingFile();
        
        asyncExec(new Runnable() {
            public void run()
            {
                setContentDescription("");
            }
        });
    }

    private void writeData(byte[] data)
    {
        if (data == null || data.length == 0)
            return;
        
        try
        {
            out.write(data);
            
            ensureTailIsVisible(data.length);
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        
        if (!dataWasWritten())
        {
            dataWasWritten = true;
            
            asyncExec(new Runnable() {
                @Override
                public void run()
                {
                    IWorkbenchSiteProgressService progressService = 
                        (IWorkbenchSiteProgressService)getSite().getAdapter(IWorkbenchSiteProgressService.class);
                    
                    progressService.incrementBusy();
                }
            });        
        }
    }
    
    private void ensureTailIsVisible(final int charsWritten)
    {
        asyncExec(new Runnable() {
            @Override
            public void run()
            {
                // TODO: make sure the last line is visible -
                
                IDocument document = textConsoleViewer.getDocument();
                
                int lastPosition = document.getLength() - 1;
                
                if (lastPosition > 0)
                {
                    try
                    {
                        IRegion lineInfo = document.getLineInformationOfOffset(lastPosition);
                        
                        if (textConsoleViewer.getBottomIndexEndOffset() < lineInfo.getOffset())
                        {
                            if (lineInfo.getLength() > 0)
                                textConsoleViewer.revealRange(lineInfo.getOffset(), 1);
                            else
                                textConsoleViewer.revealRange(lineInfo.getOffset(), 0);
                        }
                    }
                    catch (BadLocationException ble)
                    {
                        ble.printStackTrace();
                    }
                }                
            }
        });
    }





    private boolean dataWasWritten()
    {
        return dataWasWritten;
    }





    private void startListening()
    {
        LogViewerPlugin.getPlugin().addEclipseLaunchListener(this);        
    }





    private void stopListening()
    {
        LogViewerPlugin.getPlugin().removeEclipseLaunchListener(this);        
    }
    
    
    private class ClearConsoleAction
        extends Action
    {
        public ClearConsoleAction()
        {
            super("Clear Log Viewer", LogViewerPlugin.getImageDescriptor("search_rem.gif"));
        }
        
        @Override
        public void run()
        {
            console.clearConsole();
            
            if (fileWatcher == null)
            {
                asyncExec(new Runnable() {
                    public void run()
                    {
                        setContentDescription("");
                    }
                });
            }
        }
    }
    
    private class FileWatcher
        extends Job
    {
        private File    file;
        
        private long    timestamp = -1;
        private long    lastReadPoint;
        
        
        public FileWatcher(File file)
        {
            super("Log Viewer");
            
            this.file = file;
            
            setSystem(true);
            
            schedule(1000);
        }
        
        @Override
        protected IStatus run(IProgressMonitor monitor)
        {
            updateFile(monitor);
            
            schedule(200);
            
            return Status.OK_STATUS;
        }

        private void updateFile(IProgressMonitor monitor)
        {
            if (file.exists() && timestamp == -1)
            {
                byte[] data = readAll(file);
                
                writeData(data);
                
                timestamp = file.lastModified();
                lastReadPoint = file.length();
            }
            else if (!file.exists() && timestamp != -1)
            {
                handleFileDeleted();
            }
            else if (!file.exists())
            {
                return;
            }
            else if (file.lastModified() != timestamp)
            {
                byte[] data = readFrom(file, lastReadPoint);
                
                writeData(data);
                
                timestamp = file.lastModified();
                lastReadPoint = file.length();
                
                timestamp = file.lastModified();
            }
        }
        
        private byte[] readAll(File file)
        {
            return readFrom(file, 0);
        }
        
        private byte[] readFrom(File file, long readFrom)
        {
            long fileLength = file.length();
            
            if ((fileLength - readFrom) == 0)
                return new byte[0];
            
            // TODO: fix - this should not occur
            if (fileLength - readFrom < 0)
                return null;
            
            try
            {
                RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
                
                try
                {
                    byte[] data = new byte[(int)(fileLength - readFrom)];
                    
                    randomAccessFile.seek(readFrom);
                    randomAccessFile.readFully(data);
                    
                    return data;
                }
                catch (IOException ioe)
                {
                    return null;
                }
                finally
                {
                    try
                    {
                        randomAccessFile.close();
                    }
                    catch (IOException e)
                    {
                        
                    }
                }
            }
            catch (FileNotFoundException e)
            {
                return null;
            }
        }
        
        private void handleFileDeleted()
        {
            if (dataWasWritten())
            {
                console.clearConsole();
                
                dataWasWritten = false;
                
                timestamp = -1;
                lastReadPoint = 0;
                
                asyncExec(new Runnable() {
                    @Override
                    public void run()
                    {
                        IWorkbenchSiteProgressService progressService = 
                            (IWorkbenchSiteProgressService)getSite().getAdapter(IWorkbenchSiteProgressService.class);
                        
                        progressService.decrementBusy();
                    }
                });
            }
        }

        public void dispose()
        {
            cancel();
        }
    }
    
}
